/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.cloud.all.security.config.annotation;

import cn.cloud.all.security.authentication.AuthenticationEventPublisher;
import cn.cloud.all.security.authentication.AuthenticationManager;
import cn.cloud.all.security.config.ObjectPostProcessorConfiguration;
import cn.cloud.all.security.core.Authentication;
import cn.cloud.all.security.core.userdetails.UserDetailsService;
import cn.cloud.all.security.crypto.password.PasswordEncoder;
import cn.cloud.all.security.crypto.password.PasswordEncoderFactories;
import cn.cloud.all.security.oauth2.config.annotation.ObjectPostProcessor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.target.LazyInitTargetSource;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.Assert;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

@Configuration(proxyBeanMethods = false)
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {

    private AtomicBoolean buildingAuthenticationManager = new AtomicBoolean();

    private ApplicationContext applicationContext;

    private AuthenticationManager authenticationManager;

    private boolean authenticationManagerInitialized;

    private List<GlobalAuthenticationConfigurerAdapter> globalAuthConfigurers = Collections.emptyList();

    private ObjectPostProcessor<Object> objectPostProcessor;

    @Bean
    public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor, ApplicationContext context) {
        LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context);
        AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context, AuthenticationEventPublisher.class);

        DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, defaultPasswordEncoder);
        if (authenticationEventPublisher != null) {
            result.authenticationEventPublisher(authenticationEventPublisher);
        }
        return result;
    }

    @Bean
    public static GlobalAuthenticationConfigurerAdapter enableGlobalAuthenticationAutowiredConfigurer(
            ApplicationContext context) {
        return new EnableGlobalAuthenticationAutowiredConfigurer(context);
    }

    @Bean
    public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
        return new InitializeUserDetailsBeanManagerConfigurer(context);
    }

    @Bean
    public static InitializeAuthenticationProviderBeanManagerConfigurer initializeAuthenticationProviderBeanManagerConfigurer(ApplicationContext context) {
        return new InitializeAuthenticationProviderBeanManagerConfigurer(context);
    }

    public AuthenticationManager getAuthenticationManager() throws Exception {
        if (this.authenticationManagerInitialized) {
            return this.authenticationManager;
        }
        AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
        if (this.buildingAuthenticationManager.getAndSet(true)) {
            return new AuthenticationManagerDelegator(authBuilder);
        }

        for (GlobalAuthenticationConfigurerAdapter config : globalAuthConfigurers) {
            authBuilder.apply(config);
        }

        authenticationManager = authBuilder.build();

        if (authenticationManager == null) {
            authenticationManager = getAuthenticationManagerBean();
        }

        this.authenticationManagerInitialized = true;
        return authenticationManager;
    }

    @Autowired(required = false)
    public void setGlobalAuthenticationConfigurers(
            List<GlobalAuthenticationConfigurerAdapter> configurers) {
        configurers.sort(AnnotationAwareOrderComparator.INSTANCE);
        this.globalAuthConfigurers = configurers;
    }

    @Autowired
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Autowired
    public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
        this.objectPostProcessor = objectPostProcessor;
    }

    @SuppressWarnings("unchecked")
    private <T> T lazyBean(Class<T> interfaceName) {
        LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource();
        String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                applicationContext, interfaceName);
        if (beanNamesForType.length == 0) {
            return null;
        }
        String beanName;
        if (beanNamesForType.length > 1) {
            List<String> primaryBeanNames = getPrimaryBeanNames(beanNamesForType);

            Assert.isTrue(primaryBeanNames.size() != 0, () -> "Found " + beanNamesForType.length
                    + " beans for type " + interfaceName + ", but none marked as primary");
            Assert.isTrue(primaryBeanNames.size() == 1, () -> "Found " + primaryBeanNames.size()
                    + " beans for type " + interfaceName + " marked as primary");
            beanName = primaryBeanNames.get(0);
        } else {
            beanName = beanNamesForType[0];
        }

        lazyTargetSource.setTargetBeanName(beanName);
        lazyTargetSource.setBeanFactory(applicationContext);
        ProxyFactoryBean proxyFactory = new ProxyFactoryBean();
        proxyFactory = objectPostProcessor.postProcess(proxyFactory);
        proxyFactory.setTargetSource(lazyTargetSource);
        return (T) proxyFactory.getObject();
    }

    private List<String> getPrimaryBeanNames(String[] beanNamesForType) {
        List<String> list = new ArrayList<>();
        if (!(applicationContext instanceof ConfigurableApplicationContext)) {
            return Collections.emptyList();
        }
        for (String beanName : beanNamesForType) {
            if (((ConfigurableApplicationContext) applicationContext).getBeanFactory()
                    .getBeanDefinition(beanName).isPrimary()) {
                list.add(beanName);
            }
        }
        return list;
    }

    private AuthenticationManager getAuthenticationManagerBean() {
        return lazyBean(AuthenticationManager.class);
    }

    private static <T> T getBeanOrNull(ApplicationContext applicationContext, Class<T> type) {
        try {
            return applicationContext.getBean(type);
        } catch (NoSuchBeanDefinitionException notFound) {
            return null;
        }
    }

    private static class EnableGlobalAuthenticationAutowiredConfigurer extends
            GlobalAuthenticationConfigurerAdapter {
        private final ApplicationContext context;
        private static final Log logger = LogFactory
                .getLog(EnableGlobalAuthenticationAutowiredConfigurer.class);

        EnableGlobalAuthenticationAutowiredConfigurer(ApplicationContext context) {
            this.context = context;
        }

        @Override
        public void init(AuthenticationManagerBuilder auth) {
            Map<String, Object> beansWithAnnotation = context
                    .getBeansWithAnnotation(EnableGlobalAuthentication.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Eagerly initializing " + beansWithAnnotation);
            }
        }
    }

    /**
     * Prevents infinite recursion in the event that initializing the
     * AuthenticationManager.
     *
     * @author Rob Winch
     * @since 4.1.1
     */
    static final class AuthenticationManagerDelegator implements AuthenticationManager {
        private AuthenticationManagerBuilder delegateBuilder;
        private AuthenticationManager delegate;
        private final Object delegateMonitor = new Object();

        AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder) {
            Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
            this.delegateBuilder = delegateBuilder;
        }

        @Override
        public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            if (this.delegate != null) {
                return this.delegate.authenticate(authentication);
            }

            synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                    this.delegate = this.delegateBuilder.getObject();
                    this.delegateBuilder = null;
                }
            }

            return this.delegate.authenticate(authentication);
        }

        @Override
        public String toString() {
            return "AuthenticationManagerDelegator [delegate=" + this.delegate + "]";
        }
    }

    static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder {
        private PasswordEncoder defaultPasswordEncoder;

        /**
         * Creates a new instance
         *
         * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use.
         */
        DefaultPasswordEncoderAuthenticationManagerBuilder(
                ObjectPostProcessor<Object> objectPostProcessor, PasswordEncoder defaultPasswordEncoder) {
            super(objectPostProcessor);
            this.defaultPasswordEncoder = defaultPasswordEncoder;
        }

        @Override
        public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
                throws Exception {
            return super.inMemoryAuthentication()
                    .passwordEncoder(this.defaultPasswordEncoder);
        }

        @Override
        public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication()
                throws Exception {
            return super.jdbcAuthentication()
                    .passwordEncoder(this.defaultPasswordEncoder);
        }

        @Override
        public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
                T userDetailsService) throws Exception {
            return super.userDetailsService(userDetailsService)
                    .passwordEncoder(this.defaultPasswordEncoder);
        }
    }

    static class LazyPasswordEncoder implements PasswordEncoder {
        private ApplicationContext applicationContext;
        private PasswordEncoder passwordEncoder;

        LazyPasswordEncoder(ApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }

        @Override
        public String encode(CharSequence rawPassword) {
            return getPasswordEncoder().encode(rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword,
                               String encodedPassword) {
            return getPasswordEncoder().matches(rawPassword, encodedPassword);
        }

        @Override
        public boolean upgradeEncoding(String encodedPassword) {
            return getPasswordEncoder().upgradeEncoding(encodedPassword);
        }

        private PasswordEncoder getPasswordEncoder() {
            if (this.passwordEncoder != null) {
                return this.passwordEncoder;
            }
            PasswordEncoder passwordEncoder = getBeanOrNull(this.applicationContext, PasswordEncoder.class);
            if (passwordEncoder == null) {
                passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            }
            this.passwordEncoder = passwordEncoder;
            return passwordEncoder;
        }

        @Override
        public String toString() {
            return getPasswordEncoder().toString();
        }
    }
}
